/**
* project3D Engine
* @author John Sword
* @version 2 - AS3
*/

package engine.objects
{

	import engine.camera.Camera3D;
	import engine.geom.Face;
	import engine.geom.Point3D;
	import engine.geom.UV;
	import engine.geom.Vector;
	import engine.geom.Vertex;
	import engine.geom.convexHull2D;
	import engine.materials.Material;
	import engine.math.Matrix3x3;
	import engine.math.VectorUtils;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;
	import flash.geom.Point;

	public class Object3D extends EventDispatcher
	{
		/* public */
		public var faces:Array; // object faces
		public var tVertices:Array; // the mesh that will suport transformations
		public var objID:int;
		public var objName:String;
		public var material:Material;
		public var flipNormals:Boolean = false;
		public var cull:Boolean = true;
		public var isVisible:Boolean = true;
		public var facesVisible:Boolean = true;
		public var interactive:Boolean = false;
		public var pickPos:Vector = new Vector();
		public var pickUV:UV = new UV();
		public var pickFace:Face;
		public var hull:convexHull2D;
		public var OBB:Object = new Object();
		public var screenZ:Number = Infinity;
		
		/* only accessible within extended objects */
		protected var renderMe:Boolean = true;
		protected var oVertices:Array; // original object vertices information
		protected var position:Vector;
		protected var scale:Vector;
		
		/* private */
		// object transformation vectors
		protected var side:Vector; 
		protected var up:Vector;
		protected var out:Vector;
		protected var target:Vector = new Vector ();
		protected var angleRotation:Vector = new Vector();
		// bounding box
		protected var bBox:Object;
		/* static for speed */
		protected static const sin:Function = Math.sin;
		protected static const cos:Function = Math.cos;
		protected var rotMatrix:Matrix3x3 = new Matrix3x3 ();
		protected static const vo:Vertex = new Vertex();
		//private static const minA:UV = new UV( 1000, 1000 );
		//private static const maxA:UV = new UV( -1000, -1000 );
		//private static const area:UV = new UV();
		private static const max:Function 	= Math.max;
		private static const min:Function 	= Math.min;
		private static const sqrt:Function 	= Math.sqrt;
		
		public function Object3D ()
		{
			// geometry arrays
			oVertices = new Array();
			tVertices = new Array();
			faces = new Array();
			objName = "";
			// object scale
			scale = new Vector (1,1,1);
			// position vectors
			position = new Vector ();
			// set rotation vectors
			resetRotationVectors ();
		}

		public function resetRotationVectors () : void
		{
			side = new Vector (1.0, 0.0, 0.0);
			up   = new Vector (0.0, 1.0, 0.0);
			out  = new Vector (0.0, 0.0, 1.0);
		}

		public function setMaterial ( mat:Material ) : void
		{
			material = mat;
		}
		
		public function createVertex ( v:Vertex ) : int
		{
			oVertices.push(v.clone());
			tVertices.push(v);
			return oVertices.length - 1
		}
		
		public function createFace ( v1:Number, v2:Number, v3:Number, 
									uv1:UV, uv2:UV, uv3:UV, m:Material ) : void
		{
			faces.push( new Face ( tVertices[v1] as Vertex, 
						tVertices[v2] as Vertex, 
						tVertices[v3] as Vertex, uv1, uv2, uv3, m )
			);
		}
		
		public function createFaceVs ( v1:Vertex, v2:Vertex, v3:Vertex, 
									uv1:UV, uv2:UV, uv3:UV, m:Material ) : void
		{
			var vlen:int = oVertices.length;
			createVertex( v1 );
			createVertex( v2 );
			createVertex( v3 );
			faces.push( new Face ( v1, v2, v3, uv1, uv2, uv3, m ) );
		}

		public function setPosition (v:Vector) : void
		{
			position = v;
			renderMe = true;
		}
		
		public function getPosition() : Vector 
		{
			return position;
		}
		
		public function getScale () : Vector
		{
			return scale;
		}
		
		// rotates the 'up' and 'out' vectors about the 'side' vector
		public function Pitch (a:Number) : void
		{
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_axis (side, sin(a), cos(a));
			
			// rotate the 'up' and 'out' vectors
			up  = rotMatrix.vector_multiplication (up);
			out = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}
		
		// rotates the 'side' and 'out' vectors about the 'up' vector
		public function Yaw (a:Number) : void
		{
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_axis (up, sin(a), cos(a));
			
			// rotate the 'side' and 'out' vectors
			side = rotMatrix.vector_multiplication (side);
			out  = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}
		
		// rotates the 'side' and 'up' vectors about the 'out' vector
		public function Roll (a:Number) : void
		{
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_axis (out, sin(a), cos(a));
			
			// rotate the 'side' and 'up' vectors
			side = rotMatrix.vector_multiplication (side);
			up   = rotMatrix.vector_multiplication (up);
			renderMe = true;
		}
		
		// rotates around the world x-axis
		public function rotateX (a:Number) : void
		{
			// rotation matrix that will be applied
			//var matrix:Matrix3x3 = new Matrix3x3 ();
			
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_x (sin(a), cos(a));
			
			// rotate
			side = rotMatrix.vector_multiplication (side);
			up   = rotMatrix.vector_multiplication (up);
			out  = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}
		
		// rotates around the world y-axis
		public function rotateY ( a:Number ) : void
		{
			// rotation matrix that will be applied
			//var matrix:Matrix3x3 = new Matrix3x3 ();
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_y (sin(a), cos(a));
			// rotate
			side = rotMatrix.vector_multiplication (side);
			up   = rotMatrix.vector_multiplication (up);
			out  = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}
		
		// rotates around the world z-axis
		public function rotateZ (a:Number) : void
		{
			// rotation matrix that will be applied
			//var matrix:Matrix3x3 = new Matrix3x3 ();
			
			// load the elements of the rotation matrix
			rotMatrix.load_rotation_z (sin(a), cos(a));
			
			// rotate
			side = rotMatrix.vector_multiplication (side);
			up   = rotMatrix.vector_multiplication (up);
			out  = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}
		
		// rotates around center using degrees and a rotation vector
		// the rotation is fixed
		public function angleRotate ( a:Vector ) : void
		{
			resetRotationVectors();
			// load the vector into the rotation matrix
			rotMatrix.angleRotation( a );
			angleRotation = a;
			// rotate
			side = rotMatrix.vector_multiplication (side);
			up   = rotMatrix.vector_multiplication (up);
			out  = rotMatrix.vector_multiplication (out);
			renderMe = true;
		}		 
		
		public function moveXAxis (ammount:Number) : void
		{
			position.x += int(side.x * ammount);
			position.y += int(side.y * ammount);
			position.z += int(side.z * ammount);
			renderMe = true;
		}
		
		public function moveYAxis (ammount:Number) : void
		{
			position.x += int(up.x * ammount);
			position.y += int(up.y * ammount);
			position.z += int(up.z * ammount);
			renderMe = true;
		}
		
		public function moveZAxis (ammount:Number) : void
		{
			position.x += int(out.x * ammount);
			position.y -= int(out.y * ammount);
			position.z -= int(out.z * ammount);
			renderMe = true;
		}
		
		// translate along the x-axis
		public function translateX (a:Number) : void
		{
			position.x += a;
			renderMe = true;
		}

		// translate along the y-axis
		public function translateY (a:Number) : void
		{
			position.y += a;
			renderMe = true;
		}

		// translate along the z-axis
		public function translateZ (a:Number) : void
		{
			position.z += a;
			renderMe = true;
		}
		
		// makes the object look at a point
		// not sure if this is working properly for objects
		public function lookAt ( v:Vector ) : void
		{
			var v:Vector = new Vector(v.x,-v.y,v.z);
			
			// set the out vector so that the object is looking at the point
			out = VectorUtils.Normalize ( VectorUtils.difference(v,position) );
			
			// calculate the side vector from the out vector and the world y-axis (world up vector)
			side = VectorUtils.Normalize ( VectorUtils.cross( out, new Vector(0.0, 1.0, 0.0) ) );
			
			// calculate the up vector from the out and side object vectors
			up = VectorUtils.Normalize ( VectorUtils.cross ( side, out) );
			
			side = VectorUtils.negate( side );
			renderMe = true;
		}
		
		public function Scale( s:Vector ) : void
		{
			scale = s;
			renderMe = true;
		}

		public function setInteractive () : void
		{
			var tvs:Array  = tVertices; // current object vertices (transformed)
			var vtx:Vertex; // vertex obj for holding current vertex
			var i:int = tvs.length;
			while( vtx = tvs[--i] )
			{
				vtx.o = this;
			}
			interactive = true;
		}
		
		public function broadcastCLICK () : void
		{
			//trace ("broadcastCLICK " + objName);
			dispatchEvent ( new Event(MouseEvent.CLICK) );
		}
		
		public function broadcastMOUSEOVER () : void
		{
			//trace ("broadcastRollOver " + objName);
			dispatchEvent ( new Event(MouseEvent.MOUSE_OVER) );
		}
		
		public function broadcastMOUSEOUT () : void
		{
			//trace("broadcastRollOut " + objName);+ objName);
			dispatchEvent ( new Event(MouseEvent.MOUSE_OUT) );
		}
		
		public function broadcastMouseDown () : void
		{
			//trace ("broadcastMouseDown " + objName);
			dispatchEvent ( new Event(MouseEvent.MOUSE_DOWN) );
		}
		
		public function broadcastMouseUp () : void
		{
			//trace ("broadcastMouseUp " + objName);
			dispatchEvent ( new Event(MouseEvent.MOUSE_UP) );
		}

		/*
		* project vertex points to camera coordinates
		*/
		public function project ( cam:Camera3D, occlusion:Boolean = false ) : int
		{	
			var rme:Boolean = renderMe;
			var rcam:Boolean = cam.renderMe;
			
			renderMe = false;
			
			if ((!rme && !rcam) || !isVisible ) return 0;

			// get Camera parameters
			var cm:Matrix3x3 = cam.getRotationMatrix();
			var cmaa:Number = cm.aa,
			cmab:Number = cm.ab,
			cmac:Number = cm.ac,
			cmba:Number = cm.ba,
			cmbb:Number = cm.bb,
			cmbc:Number = cm.bc,
			cmca:Number = cm.ca,
			cmcb:Number = cm.cb,
			cmcc:Number = cm.cc;
			var camV:Vector = cam.getPosition();
			var camPosX:int = camV.x,
			camPosY:int = camV.y,
			camPosZ:int = camV.z,
			camEyeZ:int = cam.eyeZ, // also known as perspective or FOV
			camEyeX:int = cam.eyeX,
			camEyeY:int = cam.eyeY,
			zClipping:int = cam.zClipping,
			x:int,
			y:int,
			z:int,
			zcalc:int,
			visibleFaces:int = 0,
			w:int = cam.w,
			h:int = cam.h,
			rvx:int,
			rvy:int,
			rvz:int;	
			
			if ( rme ) 
			{
				var rm:Matrix3x3 = update_rotation_matrix (); // get object transformed vertices
				var oVs:Array = oVertices; // non transformed object (original) vertices
				var xpos:int = X,
				ypos:int = Y,
				zpos:int = Z,
				vox:int,
				voy:int,
				voz:int;
				var rmaa:Number = rm.aa,
				rmab:Number = rm.ab,
				rmac:Number = rm.ac,
				rmba:Number = rm.ba,
				rmbb:Number = rm.bb,
				rmbc:Number = rm.bc,
				rmca:Number = rm.ca,
				rmcb:Number = rm.cb,
				rmcc:Number = rm.cc,
				sx:Number = scale.x,
				sy:Number = scale.y,
				sz:Number = scale.z;
			}
			
			var tvs:Array = tVertices; // current object vertices (transformed)
			var vtx:Vertex; // vertex obj for holding current vertex
			
			if ( occlusion ) resetOBB ();
			
			// transform each vertex and create screen coordinates
			var i:int = tvs.length
			//while (vtx = tvs[--i])	
			//{
			while (--i > -1)
			{
				vtx = tvs[i] as Vertex;
				
				// apply transformations to a temporary vertex that is holding original vertex data
				if ( rme ) // only if needed 
				{	
					// points to the original vertex information
					// and applies scale
					vox = oVs[i].x * sx;
					voy = oVs[i].y * sy;
					voz = oVs[i].z * sz;
					// use rotation matrix to apply rotation transformations
					// & translate vertex to current object position
					vtx.x = int((vox * rmaa + voy * rmab + voz * rmac) + xpos);
					vtx.y = int((vox * rmba + voy * rmbb + voz * rmbc) + ypos); // Y goes down to conform with camera
					vtx.z = int((vox * rmca + voy * rmcb + voz * rmcc) + zpos);		
				}
				
				// translate the point to the camera
				x = vtx.x - camPosX;
				y = vtx.y - camPosY;
				z = vtx.z - camPosZ;
				
				// apply camera rotation matrix
				rvx = x * cmaa + y * cmab + z * cmac;
				rvy = x * cmba + y * cmbb + z * cmbc;
				rvz = x * cmca + y * cmcb + z * cmcc;
			
				// shift world to the camera to make it first person (the camera at your eye)
				z = rvz - camEyeZ;
			
				// check if vertex is visible
				//vtx.visible = ( z > -camEyeZ );
				vtx.visible = ( z > 0 );
				
				// only calculate screen coords for visible vertices
				if ( !vtx.visible )	continue;
	
				// convert to screen coordinates
				//vtx.screen.x = (camEyeX + (rvx * camEyeZ) / (z + camEyeZ)) >> 0;
				//vtx.screen.y = (camEyeY + (rvy * camEyeZ) / (z + camEyeZ)) >> 0;
				vtx.screen.x = int(camEyeX + (rvx * camEyeZ) / z);
				vtx.screen.y = int(camEyeY + (rvy * camEyeZ) / z);
				vtx.screen.z = z;
				
				// calculate bounding box/*
				if ( occlusion )
				{
					OBB.min.x = min( vtx.screen.x, OBB.min.x );
					OBB.max.x = max( vtx.screen.x, OBB.max.x );
					OBB.min.y = min( vtx.screen.y, OBB.min.y );
					OBB.max.y = max( vtx.screen.y, OBB.max.y );
					OBB.minz.z =  min( z, OBB.minz.z );
					OBB.maxz.z =  max( z, OBB.maxz.z );
				}
	
				++visibleFaces;
			}			
			
			// check if object has visible vertices which means faces can be rendered
			if (visibleFaces > 3)
			{
				facesVisible = true;
				// get distance to camera
				if ( occlusion ) {
					screenZ = distance( cam.getPosition() );
				}
				return 1;
			}

			return 0;
		}

		public function getObjectArea () : int
		{
			return int(((OBB.max.x - OBB.min.x) * (OBB.max.y - OBB.min.y)));
		}

		public function distance ( v:Vector ) : int
		{
			var xd:int = position.x - v.x;
			var yd:int = position.y - v.y;
			var zd:int = position.z - v.z;
			return sqrt(xd*xd+yd*yd+zd*zd);
		}

		public function getRotationMatrix () : Matrix3x3
		{
			return rotMatrix;
		}
		
		public function getAngleRotation () : Vector
		{
			return angleRotation;
		}
		
		// calculates the internal rotation matrix 
		public function update_rotation_matrix () : Matrix3x3
		{
			// set the elements of the rotation matrix
			rotMatrix.aa = side.x;
			rotMatrix.ab = side.y;
			rotMatrix.ac = side.z;
			
			rotMatrix.ba = up.x;
			rotMatrix.bb = up.y;
			rotMatrix.bc = up.z;

			rotMatrix.ca = out.x;
			rotMatrix.cb = out.y;
			rotMatrix.cc = out.z;
			
			return rotMatrix;
		}
		
		public function set_rotation_matrix ( m:Matrix3x3 ) : void
		{
			side.x = m.aa;
			side.y = m.ab;
			side.z = m.ac;
			
			up.x = m.ba;
			up.y = m.bb;
			up.z = m.bc;

			out.x = m.ca;
			out.y = m.cb;
			out.z = m.cc;
		}
		
		/*
		protected function transform_vertices ( transformation:Matrix3x3 ) : void
		{
			var i:Number = tVertices.length;
			// loop through the vertices to transform them
			while (i--)
			{	
				if(transformation) {
					// calculate the transformed vertices
					tVertices[i] = transformation.vertex_multiplication ( oVertices[i] );
				} else {
					// a transformation is not being performed 
					// so displace the vertices by the mesh's position
					tVertices[i].x = oVertices[i].x;
					tVertices[i].y = oVertices[i].y;
					tVertices[i].z = oVertices[i].z;
				}
				tVertices[i].x += position.x;
				tVertices[i].y += position.y; // make sure Y goes up??
				tVertices[i].z += position.z;
			}
		}
*/
		
		private function resetOBB () : void
		{
			//OBB = new Object();
			OBB.min  = new Point(Infinity,Infinity);
			OBB.max  = new Point(-Infinity,-Infinity);
			OBB.maxz = new Point3D(1,1,-Infinity);
			OBB.minz = new Point3D(1,1,Infinity)
			//OBB.size = new Point(Infinity,Infinity);
		}

		public function boundingBox() : Object
		{
			var tvs:Array = tVertices;
			var vt:Vertex;
			var v:Vector = tvs[0].screen;

			OBB.min  = new Point(v.x,v.y);
			OBB.max  = new Point(v.x,v.y);
			OBB.size = new Point(v.x,v.y);

			for each ( vt in tvs )
			{
				v = vt.screen;

				OBB.min.x = min( v.x, OBB.min.x );
				OBB.max.x = max( v.x, OBB.max.x );

				OBB.min.y = min( v.y, OBB.min.y );
				OBB.max.y = max( v.y, OBB.max.y );

				OBB.min.z = min( v.z, OBB.min.z );
				OBB.max.z = max( v.z, OBB.max.z );
			}

			OBB.size.x = OBB.max.x - OBB.min.x;
			OBB.size.y = OBB.max.y - OBB.min.y;
			OBB.size.z = OBB.max.z - OBB.min.z;

			return OBB;
		}
		
		public function get X() : Number 
		{
			return position.x;
		}
		
		public function set X ( value:Number ) : void 
		{
			position.x = value;
			renderMe = true;
		}
	
		public function get Y() : Number 
		{
			return position.y;
		}
		
		public function set Y ( value:Number ) : void 
		{
			position.y = value;
			renderMe = true;
		}
	
		public function get Z() : Number 
		{
			return position.z;
		}
		
		public function set Z ( value:Number ) : void 
		{
			position.z = value;
			renderMe = true;
		}
	
	}

}